from __future__ import print_function

"""
File RAE.py
Author:
chengkai
"""

import threading
from myAI.state import PrintState, EvaluateParameters
import time
import numpy
import math
import random

import types
import sys, pprint
import os


from myAI.dataStructures import rL_APE
from myAI.APE_stack import print_entire_stack, print_stack_size

############################################################

### for debugging

verbose = 0


def verbosity(level):
    """
    Specify how much debugging printout to produce:

    verbosity(0) makes RAE1 and RAEplan run silently; the only printout will be
    whatever the domain author has put into the commands and methods.

    verbosity(1) prints messages at the start and end, and
    print the name and args of each task and command.

    verbosity(2) makes RAE1 and RAEplan also print the states after executing commands.
    """

    global verbose
    verbose = level
    if level > 0:
        # import colorama
        pass


# Both RAE and RAEplan
TASKS = {}  # dictionary of tasknames and the task parameters
methods = {}  # dictionary of the list of methods for every task, initialized once for every run via the domain file

# Only RAE1
commands = {}  # dictionary of commands, initialized once for every run via the domain file
raeLocals = rL_APE()  # APE variables that are local to every stack

############################################################
# Functions to tell Rae1 what the commands and methods are

def declare_commands(cmd_list):
    """
    Call this after defining the commands, to tell APE and APE-plan what they are.
    cmd_list must be a list of functions, not strings.
    """
    commands.update({cmd.__name__: cmd for cmd in cmd_list})
    return commands


def GetCommand(cmd):
    """
        Get the actual operational model of the command 
    """
    name = cmd.__name__
    return commands[name]


class MethodInstance():
    def __init__(self, m):
        self.method = m
        self.params = None

    def SetParams(self, p):
        self.params = p

    def Call(self):
        self.method(*self.params)

    def GetName(self):
        return self.method.__name__

    def __repr__(self):
        return self.method.__name__ + str(self.params)

    def __eq__(self, other):
        if other == 'heuristic' or other == 'Failure' or other == None or other == 'root' or other == 'task':
            return False
        else:
            return self.method == other.method and self.params == other.params


def declare_task(t, *args):
    TASKS[t] = args


# declares the refinement methods for a task;
# ensuring that some constraints are satisfied
# def declare_methods(task_name, *method_list):
#
#     taskArgs = TASKS[task_name]
#     q = len(taskArgs)
#
#     methods[task_name] = []
#     for m in method_list:
#
#         variableArgs = False
#         if len(taskArgs) == 1:
#             if taskArgs[0] == "*":
#                 variableArgs = True
#         if variableArgs != True:
#             # ensure that the method has atleast as many parameters as the task
#             assert(m.__code__.co_argcount >= q)
#
#             # ensure that the variable names of the
#             # first q parameters of m match with the parameters of task t
#             assert(m.__code__.co_varnames[0:q] == taskArgs)
#
#         methods[task_name].append(m)

def declare_methods(task_name, *method_list):
    """
    Call this once for each task, to tell Rae1 what the methods are.
    task_name must be a string.
    method_list must be a list of functions, not strings.
    """
    methods.update({task_name: list(method_list)})
    return methods[task_name]


def GetMethodInstances(methods, tArgs):
    instanceList = []  # List of all applicable method instances for t
    #方法实例化时，将ai对象也作为方法的参数
    # new_tArgs = tArgs+[myAgent]
    for m in methods:
        q = m.__code__.co_argcount
        mArgs = m.__code__.co_varnames

        if len(tArgs) < q:
            # some parameters are uninstantiated
            paramList = EvaluateParameters(m.parameters, mArgs, tArgs)

            for params in paramList:
                instance = MethodInstance(m)
                instance.SetParams(tArgs + params)
                instanceList.append(instance)
        else:
            instance = MethodInstance(m)
            instance.SetParams(tArgs)
            instanceList.append(instance)

        # instance = MethodInstance(m)
        # instance.SetParams(new_tArgs)
        # instanceList.append(instance)
    return instanceList

############################################################
# The user can use these to see what the commands and methods are.

def print_commands(olist=commands):
    """Print out the names of the commands"""
    print('commands:', ', '.join(olist))

def print_methods(mlist=methods):
    """Print out a table of what the methods are for each task"""
    print('{:<14}{}'.format('TASK:', 'METHODS:'))
    for task in mlist:
        print('{:<14}'.format(task) + ', '.join([f.__name__ for f in mlist[task]]))


############################################################
# Stuff for debugging printout

class Failed_command(Exception):
    pass


class Failed_Rollout(Exception):
    pass


class Failed_task(Exception):
    pass


class Incorrect_return_code(Exception):
    pass


class Expanded_Search_Tree_Node(Exception):
    pass


class DepthLimitReached(Exception):
    pass


# ****************************************************************
# Functions to control Progress of each stack step by step
class IpcArgs():
    """ IPCArgs is just a collection of variable bindings to share data among the threads."""

    def __init__(self):
        # 这里设置信号量计数为1，表示同时只能有1个线程可以用这个锁
        # 相当设置了线程可同时并发的数量,这允许了多个线程可以同时访问相同的代码区
        self.sem = threading.Semaphore(1)  # the semaphore to control progress of each stack and master
        # 这里的master thread就是指的raeMult这个线程本身
        # 通过nextStack的值，指定哪个任务堆栈可以运行，0表示master thread运行
        # master thread线程的作用就是不断的轮询各个任务堆栈，使各任务堆栈能够交错执行
        self.nextStack = -1  # the master thread is the next in line to be executed, which adds a new stack for every new task
        self.stack_signal = {}      #{obj_id: 0/1}  0表示暂停，1表示执行
        self.threadDict = {}    #{obj_id:[thread,task]} 保存task的目的是便于将线程中执行的任务与上层AI分配的任务比较，thread创建的线程对象，task任务信息
        self.total_actions = []     #用于保存各个任务堆栈返回的动作action
        self.mutex_actions = threading.Lock()
        self.mutex_task = threading.Lock()
        self.mutex_observation = threading.Lock()
        self.mutex_basic_situ = threading.Lock()
        self.mutex_threat_situ = threading.Lock()
        self.mutex_situation = threading.Lock()


    def ipcargs_reset(self):
        self.sem = None
        self.nextStack = None
        self.stack_signal = None
        self.threadDict = None
        self.total_actions = None
        self.mutex_actions = None
        self.mutex_task = None
        self.mutex_observation = None
        self.mutex_basic_situ = None
        self.mutex_threat_situ = None
        self.mutex_situation = None

# 全局对象
# ipcArgs = IpcArgs()


def BeginCriticalRegion(stackid, ipcArgs):
    while (ipcArgs.stack_signal[stackid] != 1):
        time.sleep(0.05)



def EndCriticalRegion(stackid, ipcArgs):
    ipcArgs.stack_signal[stackid] = 0



# ****************************************************************

############################################################
# The actual acting engine    

def RAE1(task, raeArgs, myAgent):
    """
    RAE1 is the actor with a single execution stack. The first argument is the name (which
    should be a string) of the task to accomplish, and raeArgs are the arguments for the task and other stack parameters.
    raeArgs has a stack id, stack, and parameters to the task, taskArgs. 
    """
    InitializeStackLocals(task, raeArgs)

    # 判断当前任务堆栈是否是激活的，如果是则该线程能够继续运行，否则需要等待本线程激活
    # BeginCriticalRegion(raeLocals.GetStackId(), myAgent.agenda.ipcArgs)

    try:
        taskArgs = raeArgs.taskArgs + [myAgent]
        retcode = do_task(task, *taskArgs)  # do acting
        # retcode = DoTaskInRealWorld(task, taskArgs, myAgent)

    except Failed_command as e:
        retcode = 'Failure'

    except Failed_task as e:
        retcode = 'Failure'

    # 捕获线程错误异常
    # except Exception as e:
    #     # print("thread error: " + str(e))
    #     retcode = 'Error'
    else:
        pass

    return retcode


def InitializeStackLocals(task, raeArgs):
    """ Initialize the local variables of a stack used during acting """
    raeLocals.SetStackId(raeArgs.stack)  # to keep track of the id of the current stack.
    # raeLocals.SetRetryCount(0)  # to keep track of the number of retries in the current stack. This is used to calculate retry ratio
    # raeLocals.SetCommandCount({})  # to keep track of the number of instances of every commands executed. Used to calculate the speed to success
    # This is a dictionary to accomodate for commands with different costs.
    raeLocals.SetMainTask(task)
    raeLocals.SetMainTaskArgs(raeArgs.taskArgs)

def choose_candidate(candidates, task, taskArgs):
    #当有不止一个方法时，选择第一个
    if len(candidates) > 0:
        # random.shuffle(candidates)
        return (candidates[0], candidates[1:])
    else:
        pass

def do_task(task, *taskArgs):
    return DoTaskInRealWorld(task, taskArgs)

def DoTaskInRealWorld(task, taskArgs):
    """
    Function to do the task in real world
    """

    retcode = 'Failure'
    candidateMethods = methods[task][:]
    candidates = GetMethodInstances(candidateMethods, taskArgs)
    if candidates == []:
        raise Failed_task('{}{}'.format(task, taskArgs))
    while (retcode != 'Success'):
        (m, candidates) = choose_candidate(candidates, task, taskArgs)
        retcode = CallMethod_OperationalModel(raeLocals.GetStackId(), m, taskArgs)
        if candidates == []:
            break
        # if retcode == 'Failure':
        #     raeLocals.SetRetryCount(raeLocals.GetRetryCount() + 1)
    if retcode == 'Failure':
        raise Failed_task('{}{}'.format(task, taskArgs))
    elif retcode == 'Success':
        return retcode
    else:
        raise Incorrect_return_code('{} for {}{}'.format(retcode, task, taskArgs))


def CallMethod_OperationalModel(stackid, m, taskArgs):
    retcode = 'Failure'
    try:
        m.Call()  # This is the main job of this function, CallMethod
        retcode = 'Success'
    except Failed_command as e:
        pass
    except Failed_task as e:
        pass

    return retcode

# 执行command，改变state
def beginCommand(cmd, cmdRet, cmdArgs):
    cmdPtr = GetCommand(cmd)
    # command执行返回的结果：1.动作 2.Success 3.Failure 4.None
    cmdRet['state'] = cmdPtr(*cmdArgs)
    if cmdRet['state'] == 'Success':
        pass
    elif cmdRet['state'] == 'Failure':
        pass
    elif cmdRet['state'] is None:
        pass
    else:
        myAgent = cmdArgs[-1]
        # myAgent.agenda.ipcArgs.mutex_actions.acquire()
        if cmdRet['state'] not in myAgent.agenda.ipcArgs.total_actions:
            myAgent.agenda.ipcArgs.total_actions.append(cmdRet['state'])
            # print(f'rae current step: {myAgent.observation["time"]["cur_step"]}     actions: {cmdRet["state"]}')
        # myAgent.agenda.ipcArgs.mutex_actions.release()

def do_command(cmd, *cmdArgs):
    """
    Perform command cmd(cmdArgs).
    """

    return DoCommandInRealWorld(cmd, cmdArgs)


def DoCommandInRealWorld(cmd, cmdArgs):

    cmdRet = {'state': 'begin'}
    beginCommand(cmd, cmdRet, cmdArgs)
    retcode = cmdRet['state']
    #只要不是空动作，就进行计数
    # if retcode is not None:
    #     #统计command的数量
    #     if cmd.__name__ in raeLocals.GetCommandCount():
    #         raeLocals.GetCommandCount()[cmd.__name__] += 1
    #     else:
    #         raeLocals.GetCommandCount()[cmd.__name__] = 1


    # 如果失败，不堵塞本线程，回溯，尝试其他细化方法
    # 两种情况：
    # 1.有可行的其他方法，执行一个成功动作，再阻塞本线程，释放激活权限；
    # 2.没有其他可行方法，回溯到根节点任务，返回失败，释放激活权限，本线程结束
    if retcode == 'Failure':
        raise Failed_command('{}{}'.format(cmd.__name__, cmdArgs))
    # 如果执行成功，阻塞本线程，释放激活权限
    # elif retcode == 'Success':
    # 只要不是Failure，表示动作成功执行（包括Success，None，具体动作），返回Success标志
    else:
        EndCriticalRegion(raeLocals.GetStackId(), cmdArgs[-1].agenda.ipcArgs)
        BeginCriticalRegion(raeLocals.GetStackId(), cmdArgs[-1].agenda.ipcArgs)
        # return retcode
        return 'Success'
    # else:
    #     raise Incorrect_return_code('{} for {}{}'.format(retcode, cmd.__name__, cmdArgs))









